【看表情包学Linux】进程阻塞 您所在的位置:网站首页 c语言中 status 【看表情包学Linux】进程阻塞

【看表情包学Linux】进程阻塞

2023-03-15 22:47| 来源: 网络整理| 查看: 265

  🤣 爆笑教程 👉 《看表情包学Linux》👈 猛戳订阅  🔥

💭 写在前面:大家好,我是柠檬叶子C,在上一章中我们讲了 waitpid 的 status 参数,本章我们讲解它的 options 参数。在讲解之前我们需要理解进程阻塞,介绍非阻塞的轮询检测机制,学会 waitpid。然后我们重点讲解二进程程序替换,这是本章的重点,如何让子进程执行一个新的程序?本章之前,我们都是让子进程执行父进程代码的,本章我们让子进程能够开启属于自己的 "全新旅程",这听上去很 cool,通过讲解进程替换的替换原理,然后先介绍一个进程替换函数 execl,通过介绍这个函数来打开突破口,引入进程创建的知识点。最后,我们在学习进程创建的 exec 函数簇,这里面有很多形似 execl 的,各种关于替换的函数,因为它们的功能大同小异,所以我们称之为函数簇。话就说到这里,让我们速速开始今天的学习!

📃 本章目录:

Ⅰ. 进程阻塞(Process Blocking)

0x00 回顾:继续讲解 waitpid

0x01 如何理解进程阻塞?

0x02 轮询检测(Polling)

0x03 基于非阻塞的轮询等待(waitpid)

Ⅱ. 进程程序替换(Process Substitution)

0x00 引入:让子进程执行一个新的程序

0x01 程序替换原理

0x02 以可变参数列表的接收参数的 execl 接口

0x03 引入进程创建

Ⅲ. exec 函数簇(Sheaf of functions exec)

0x00 以指针数组接收参数的 execv 接口

0x01 无需带路径就能直接执行的 execlp 接口(可变参数列表)

0x02 无需带路径的 execvp 接口(指针数组)

0x03 利用 exec 调各种程序(可耦合各种语言)

0x04 添加环境变量给目标进程的 execle 接口

0x05 也是环境变量,但是拿数组传的 execve 接口

0x06 超级缝合怪 execvpe 接口

0x07 为什么会有这么多 exec 接口?

  本篇博客全站热榜排名:未上榜

Ⅰ. 进程阻塞(Process Blocking) 0x00 回顾:继续讲解 waitpid

 我们先来简单回顾一下上一章的内容:

#include #include pid_t waitpid(pid_t pid, int* status, int options);

上一章介绍了 status 参数,知道了如何通过位操作来截 status 获取进程错误码与错误信号:

status&0x7F // 获取错误信号 (status>>8)&0xFF // 获取错误码

 使用这种方式前提是你必须得 status 位图的构成,操作系统设计成接口的初衷是让你用起来更简单,所以准备了宏。当然,如果你就用位操作来获取也是可以的。但是我们还是不建议这么做,因为直接用操作系统提供的宏就好了,我们可以通过 WIFEXITED 宏来检测子进程是否正常退出(检测进程退出时信号是否为 0),在用 WEXITSTAUS 宏还获取进程的退出码:

if (WIFEEXITED(status)) { printf("等待成功: exit code: %d\n", WIFEEXITED(status)); }

这些都是上一章讲解 status 参数的内容了,我们下面要讲的是 waitpid 另一个参数 options。 

当 options 为 0,则标识为 阻塞等待 (关于阻塞等待的概念我们之前将进程状态时说过)

比如:如果子进程不退出,父进程在等,等的时候子进程是卡在那等的,在用户的角度就是:

" 沃日怎么卡了"

 这种用户级的现象就叫作阻塞等待!

0x01 如何理解进程阻塞?

❓ 思考:如何理解父进程进程阻塞?

首先,进程状态我们说过:如果一个进程在系统层面上要等待某件事情发生,

但这件事情还没发生,那么当前进程的代码还没法向后运行,只能让该进程处于阻塞状态。

就是让父进程的 task_struct 状态由 R\rightarrow S,从运行队列投入到等待队列,等待子进程退出。

子进程退出的本质是条件就绪,如果子进程退出条件一旦就绪,操作系统会逆向地做上述工作。

将父进程的 \textrm{pcb} 从等待队列再搬回运行队列,并将状态 S\rightarrow R,此时父进程就会继续运行。

0x02 轮询检测(Polling)

所谓的阻塞,其实就是挂起。在上层表现来看,就是进程卡住了(比如 scanf,cin 等)。

阻塞式等待就是 "傻等",就是在等的时侯父进程什么也不做,就坐那傻等。

  

而非阻塞式等待是 "巧等",会做些自己的事,而不是一屁股做那傻等!

多次调用非阻塞接口,这个过程我们称之为 轮询检测 (Polling)。

"轮询检测检测期间,你不退,我就直接返回做我自己的事情去啦"

我们上一章中讲解 waitpid 时,举的例子都是 阻塞式 的等待。

如果我们想 非阻塞式 的等,我们可以设置 options 选项为 WNOHANG (With No Hang)。

这个选项通过字面很好理解,就是等待的时候不要给我挂 (Hang) 住,其实就是非阻塞!

现在我们正式介绍一下 waitpid 的返回值:

如果此时等待成功,返回值是子进程的退出码。如果你是非阻塞等待 (WNOHANG),等待的子进程没有退出,返回值为 0。

0x03 基于非阻塞的轮询等待(waitpid)

如果我们想把我们上一章节,演示 waitpid 使用方式的代码,改为非阻塞等待。

 我们只需要将 waitpid 的 options 参数加上。

💬 代码演示:基于非阻塞的轮询等待

#include #include #include #include #include #include int main(void) { pid_t id = fork(); if (id == 0) { // 子进程 while (1) { printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid()); sleep(5); // 先睡眠 5s,5s后退出 break; } exit(233); } else if (id > 0) { // 父进程 /* 基于非阻塞的轮询等待方案 */ int status = 0; while (1) { pid_t ret = waitpid(-1, &status, WNOHANG); if (ret > 0) { // 等待成功 printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF); } else if (ret == 0) { // 等待成功,但是子进程没有退出 printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n"); sleep(1); } else { // 出错了,暂时不作处理 } } } else { // 什么也不做 } }

💡 说明:我们只需要将 waitpid 中的 options 参数带上 WHOHANG 就可以了。返回值 ret>0 就是等待成功,我们这里新增一个等于 0 的判断,作为 "等待成功但是子进程还没有退出" 的情况,因为等待的子进程没有退出,返回值为 0 。运行后,就会问子进程好没好,如果没有好父进程就可以做自己的事情了,而不是在那傻等子进程!这,就是非阻塞式轮询等待。

 🚩 运行结果如下:

我们可以让父进程在非阻塞等待时真正做点事,别让父进程在等子进程闲着。

💬 代码演示:因为要使用 vector,我们创建 .cpp 文件

#include #include #include #include #include #include #include typedef void (* handler_t)(); // 函数指针类型 // 方法集 std::vector handlers; void func1() { printf("Hello,我是方法1\n"); } void func2() { printf("Hello,我是方法2\n"); } void Load() { // 加载方法 handlers.push_back(func1); handlers.push_back(func2); } int main(void) { pid_t id = fork(); if (id == 0) { // 子进程 while (1) { printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid()); sleep(3); } exit(233); } else if (id > 0) { // 父进程 /* 基于非阻塞的轮训等待方案 */ int status = 0; while (1) { pid_t ret = waitpid(-1, &status, WNOHANG); if (ret > 0) { // 等待成功 printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF); } else if (ret == 0) { // 等待成功,但是子进程没有退出 printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n"); if (handlers.empty()) { Load(); } for (auto f : handlers) { f(); // 回调处理对应的任务 } sleep(1); } else { // 出错了,暂时不作处理 } } } else { // 什么也不做 } }

如果你想要你的程序直接父进程做更多的事情,把方法加到 Load 里就可以了。

 写下 Makefile:

mytest:mytest.cpp g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f mytest

🚩 运行结果如下:

Ⅱ. 进程程序替换(Process Substitution) 0x00 引入:让子进程执行一个新的程序

我们之前做的所有代码演示,子进程执行的都是父进程的代码片段。

 如果我们想让创建出来的子进程,执行全新的程序呢?

回顾:我们曾经创建的子进程和父进程是代码共享的,通过 if-else  同时执行(写时拷贝),经过同一个变量通过虚拟地址转化为物理地址,让父子进程得到不同的值,从而判断出来让父子进程执行不同的代码片段。这是我们之前的操作。

之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换。

a238ee82fbb64fa0a1898b7c1b4e6552.jpeg为什么要进行程序替换?因为我们想让我们的子进程执行一个全新的程序。

那为什么要让子进程执行新的程序呢?

我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:

让子进程执行父进程的代码片段(服务器代码…)想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)

 然而,这些操作都是真的,这不是梦 ~

0x01 程序替换原理

📃 程序替换的原理:

将磁盘中的内存,加载入内存结构。重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序!

这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!

因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。

 内核数据结构没有发生任何变化! 包括子进程的 \textrm{pid}\textrm{pid} 都不变,说明压根没有创建新进程。

0x02 以可变参数列表的接收参数的 execl 接口

我们要调用接口,让操作系统去完成这个工作 —— 系统调用。

如何进行程序替换?我们先见见猪跑 —— 从 execl 这个接口讲,看看它怎么跑的。

int execl(const char* path, const char& arg, ...);

如果我们想执行一个全新的程序,我们需要做几件事情:

(要执行一个全新的程序,以我们目前的认识,程序的本质就是磁盘上的文件)

第一件事情:先找到这个程序在哪里。第二件事情:程序可能携带选项进行执行(也可以不携带)。

明确告诉 OS,我想怎么执行这个程序?要不要带选项。

\Rightarrow 简单来说就是:① 程序在哪?  ② 怎么执行?

 所以,execl 这个接口就必须得把这两个功能都体现出来!

它的第一个参数是 path,属于路径。参数  const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。

💬 代码演示:exec()

#include #include int main(void) { printf("我是一个进程,我的PID是:%d\n", getpid()); // ls -a -l execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 带选项 printf("我执行完毕了,我的PID是:%d\n", getpid()); return 0; }

🚩 运行结果如下:

 刚才是带选项的,现在我们再来演示一下不带选项的: 

#include #include int main(void) { printf("我是一个进程,我的PID是:%d\n", getpid()); // top execl("/usr/bin/top", "top", NULL); // 不带选项 printf("我执行完毕了,我的PID是:%d\n", getpid()); return 0; }

 🚩 运行结果如下:

这样我们的程序就直接能执行 top 命令了,除此之外,我们曾经学的大部分命令其实都可以通过 execl 执行起来。这就叫做 程序替换。

不知道大家有没有发现问题?代码和输出结果有什么不对劲的地方?

最后一句代码 ——  "我执行完毕了,我的PID是" 似乎没有打印出来啊?

 为什么我们最后的代码并没有被打印出来?

因为 一旦替换成功,是会将当前进程的代码和数据全部替换的!

所以自然后面的 printf 代码早就被替换了,这意味着该代码不复存在了,荡然无存!

因为在程序替换的时候,就已经把对应进程的代码和数据替换掉了!

而第一个 printf 执行了的原因自然是因为程序还没有执行替换,

调了 execl 后的所有代码就会被:

" 全 部 曹 飞 "

所以,这里的程序替换函数用不用判断返回值?为什么?

int ret = execl(...);

一旦替换成功,还会执行返回语句吗?返回值有意义吗? 没有意义的!

 程序替换不用判断返回值!因为只要成功了,就不会有返回值。 而失败的时候,必然会继续向后执行。通过返回值最多能得到是什么原因导致替换失败的。只要执行了后面的代码,看都不用看,一定是替换失败了;只要有返回值,就一定是替换失败了。

我们来模拟一下失败的情况,我们来执行一个不存在的指令 giveMeMoney:

#include #include int main(void) { printf("我是一个进程,我的PID是:%d\n", getpid()); // 我们用 ret 接收函数返回值结果 int ret = execl("/usr/bin/giveMeMoney", "ls", "-l", "-a", NULL); printf("我执行完毕了,我的PID是:%d,ret: %d\n", getpid(), ret); return 0; }

🚩 运行结果如下:

💡 说明:execl 替换失败,就会继续向后执行。但是,一旦 execl 成功后就会跟着新程序的逻辑走,就不会再 return 了,再也不回来了,所以返回值加不加无所谓了。

* 先打个预防针:下面我们会讲解一大坨 exec 函数簇,特性和这个 execl 大同小异!

0x03 引入进程创建

"子进程:以前我没得选,现在我免费了!我要脱离父进程的怀抱了!(叛逆期)"

以前我们的示例都是让子进程执行父进程的代码,我们今天想让子进程执行自己的程序。

#include #include #include #include int main(void) { printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id = fork(); if (id == 0) { /* child 我们想让子进程执行全新的程序 */ printf("我是子进程,我的PID是:%d\n", getpid()); execl("/usr/bin/ls", "ls", "-a", "-l", NULL); /* 让子进程执行替换 */ exit(1); /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/ } /* 一定是父进程 */ int status = 0; int ret = waitpid(id, &status, 0); if (ret == id) { /* 等待成功 */ sleep(2); printf("父进程等待成功!\n"); } return( 0); }

🚩 运行结果如下:

💡 说明:成功执行代码,父进程也等待成功了。这里的子进程没有执行父进程的代码,而是执行了自己的程序,它开启了自己的旅程,执行了 execl 替换后直接高高兴兴地扬长而去了。

 诶……子进程执行程序替换,会不会影响父进程呢?不会!因为进程具有独立性。

为什么?如何做到的?子进程是如何做到代码和数据做分离的呢?

让子进程与父进程做相似的代码片段,子进程改了,父进程也不受影响。

我们在前几章,讲过数据层面发生写时拷贝的概念。

我们说过:fork 之后父子是共享的,如果要替换新的程序我能理解把新的程序的代码加载到内存里,我的子进程新的代码程序出来之后发生数据的写时拷贝,生成新的数据段。

不是说代码是共享的吗?我们该如何去理解呢?

当程序替换的时候,我们可以理解成 —— 代码和数据都发生了写时拷贝,完成了父子分离。

Ⅲ. exec 函数簇(Sheaf of functions exec) 0x00 以指针数组接收参数的 execv 接口

刚才我们学会了 execl 接口,我们下面开始学习更多的 exec 接口!它们都是用来替换的。

下面我们先来讲解一下和 execl 很近似的 execv:

int execv(const char* path, char* const argv[]);

path 参数和 execl 一样,关注的都是 "如何找到" 

argv[] 参数关注的是 "如何执行",是个指针数组,放 char* 类型,指向一个个字符串。

大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。

所以 execv 和 execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。

值得注意的是,在构建 argv[] 的时,结尾仍然是要加上 NULL!

💬 代码演示:execv()

#include #include #include #include int main(void) { printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id = fork(); if (id == 0) { /* child 我们想让子进程执行全新的程序 */ printf("我是子进程,我的PID是:%d\n", getpid()); char* const argv_[] = { (char*)"-ls", /* 这里强转消warning*/ (char*)"-l", (char*)"-a", NULL }; execv("usr/bin/ls", argv_); /* 只是变为数组传参了而已 */ exit(1); /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/ } /* 一定是父进程 */ int status = 0; int ret = waitpid(id, &status, 0); if (ret == id) { /* 等待成功 */ sleep(2); printf("父进程等待成功!\n"); } return( 0); }

🚩 运行结果如下:

关于 execl 和 execv,我们可以这么记:

乱入的小贴士

批量化注释:ctrl+v  ->  hjkl 选中区域  -> 切换大写,输入 I    //, esc

去注释:小写,ctrl+v -> hjkl 选中区域(注释区域), d

0x01 无需带路径就能直接执行的 execlp 接口(可变参数列表) int execlp(const char* file, const char* arg, ...);

execlp,它的作用和 execv、execl 是一样的,它的作用也是执行一个新的程序。

 仍然是需要两步:① 找到这个程序   ② 告诉我怎么执行

第一个参数 file 也是 "你想执行什么程序",第二个参数 arg 是 "如何去执行它"。

所以这一块的参数传递,和 execl 是一样的,唯一的区别是比 execl 多了一个 p!

我们执行指令的时候,默认的搜索路径在环境变量 \textrm{PATH} 中,所以这个 p 的意思是环境变量。

这意味着:执行 execlp 时,会直接在环境变量中找,不用去输路径了,只要程序名即可。

我们上面例子中的代码,无论是 execl 还是 execv,执行程序都得带上路径。

而 execlp 可以不带路径,只说出你要执行哪一个程序即可,美滋滋:

execlp("ls", "ls", "-a", "-l", "NULL"); // 路径都不用,直接扔

 值得一提的是:这里出现的两个 ls 含义是不一样的,是不可以省略滴。

"老老实实敲,切勿投机取巧!"

第一个参数是 "供系统去找你是谁的",后面的一坨代表的是 "你想怎么去执行它" 。

💬 代码演示:execlp()

#include #include #include #include int main(void) { printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id = fork(); if (id == 0) { /* child 我们想让子进程执行全新的程序 */ printf("我是子进程,我的PID是:%d\n", getpid()); execlp("ls", "ls", "-a", "-l", NULL); exit(1); /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/ } /* 一定是父进程 */ int status = 0; int ret = waitpid(id, &status, 0); if (ret == id) { /* 等待成功 */ sleep(2); printf("父进程等待成功!\n"); } return( 0); }

🚩 运行结果如下:

0x02 无需带路径的 execvp 接口(指针数组) int execvp(const char* file, char* const argv[]);

看到这里,想必大家光看到这个接口的名字,就能猜到它是什么意思了。

execvp 也是带 p 的,执行 execvp 时,会直接在环境变量中找,只要程序名即可。

简单来说就是 execv 的带 p 版本罢了,将命令行参数字符串,统一放入数组中即可完成调用。

想必大家早已轻车熟路了,我们直接演示就完事了!

💬 代码演示:execvp()

#include #include #include #include int main(void) { printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id = fork(); if (id == 0) { /* child 我们想让子进程执行全新的程序 */ printf("我是子进程,我的PID是:%d\n", getpid()); char* const __argv[] = { (char*)"top", NULL }; execvp("top", __argv); // 只需要给个名就行 exit(1); /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/ } /* 一定是父进程 */ int status = 0; int ret = waitpid(id, &status, 0); if (ret == id) { /* 等待成功 */ sleep(2); printf("父进程等待成功!\n"); } return( 0); }

🚩 运行结果如下:

0x03 利用 exec 调各种程序(可耦合各种语言)

目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的 C/C++ 程序呢?

如果我们要执行其他语言写的程序呢?在回答这个问题前,我们先补充一下 Makefile 的知识。

Makefile 知识点补充

我们前几章写的 Makefile 文件只能形成一个可执行程序,现在我们学习如何形成多个。

比如,如果我们想一口气形成 2 个 可执行程序:

假设有两个可执行程序:mycmd.cpp & mytest.c,我们期望用 mytest.c 调用 mycmp.cpp:

/* mycmd.cpp */ #include int main(void) { std::cout


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有